﻿// Copyright (c) 2012-2013 fancidev
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.

#if false

using System;
using System.IO;
using System.Reflection;
using System.Text;

namespace Util.IO.Serialization
{
    public static class MemberInfoExtensions
    {
        public static TAttribute GetFirstAttribute<TAttribute>(this MemberInfo member)
            where TAttribute : Attribute
        {
            foreach (TAttribute a in member.GetCustomAttributes(typeof(TAttribute), false))
            {
                return a;
            }
            return null;
        }

        public static Type GetFieldOrPropertyType(this MemberInfo member)
        {
            if (member is FieldInfo)
                return (member as FieldInfo).FieldType;
            if (member is PropertyInfo)
                return (member as PropertyInfo).PropertyType;
            return null;
        }

        public static void SetFieldOrPropertyValue(this MemberInfo member, object obj, object val)
        {
            if (member is FieldInfo)
                (member as FieldInfo).SetValue(obj, val);
            else if (member is PropertyInfo)
                (member as PropertyInfo).SetValue(obj, val, null);
            else
                throw new ArgumentException("member is not a field or property.");
        }
    }

    /// <summary>
    /// Provide methods to serialize or deserialize an object in packed binary
    /// format.
    /// </summary>
    public class PackedSerializer
    {
        // Deserializes an object of the given type from a buffer.
        private static object Deserialize(
            byte[] buffer, int startIndex, Type t, MemberInfo info)
        {
            // Handle non-struct primitive types.
            if (t.IsEnum) // Convert enum to its underlying type
            {
                t = Enum.GetUnderlyingType(t);
            }
            if (t.IsPrimitive) // int, double, bool, etc
            {
                if (t == typeof(char))
                    return BitConverter.ToChar(buffer, startIndex);
                if (t == typeof(byte))
                    return buffer[startIndex];
                if (t == typeof(sbyte))
                    return (sbyte)buffer[startIndex];
                if (t == typeof(short))
                    return BitConverter.ToInt16(buffer, startIndex);
                if (t == typeof(ushort))
                    return BitConverter.ToUInt16(buffer, startIndex);
                if (t == typeof(int))
                    return BitConverter.ToInt32(buffer, startIndex);
                if (t == typeof(uint))
                    return BitConverter.ToUInt32(buffer, startIndex);
                if (t == typeof(long))
                    return BitConverter.ToInt64(buffer, startIndex);
                if (t == typeof(ulong))
                    return BitConverter.ToUInt64(buffer, startIndex);
                if (t == typeof(float))
                    return BitConverter.ToSingle(buffer, startIndex);
                if (t == typeof(double))
                    return BitConverter.ToDouble(buffer, startIndex);

                // We're left with: bool, IntPtr, UIntPtr, and their boxed types.
                throw new NotSupportedException(
                    "Type " + t.ToString() + " is not supported for deserialization.");
            }

            // Handle strings. The PackedSizeAttribute must be associated with
            // the field or property which specifies the number of bytes, n, 
            // to store the string and the terminating NULL byte. 
            // When reading from a stream, the bytes are read up to the first
            // NULL byte or until n bytes are read.
            // When writing to a stream, at most n - 1 bytes are written, and
            // the buffer is always terminated (and padded if necessary) with
            // NULLs.
            if (t == typeof(string))
            {
                var aSize = info.GetFirstAttribute<PackedSizeAttribute>();
                if (aSize == null)
                {
                    throw new ArgumentException(info.ToString() +
                        " must be marked with PackedSizeAttribute in order to be serialized.");
                }
                int n = Array.IndexOf(buffer, (byte)0, startIndex, aSize.Size);
                if (n < 0)
                    n = aSize.Size;
                else
                    n = n - startIndex;
                return Encoding.UTF8.GetString(buffer, startIndex, n);
            }

            // Handle arrays. The PackedSizeAttribute must be associated with
            // the field or property which specifies the number of elements 
            // in the array.
            if (t.IsArray)
            {
                var aCount = info.GetFirstAttribute<PackedCountAttribute>();
                if (aCount == null)
                {
                    throw new ArgumentException(info.ToString() +
                        " must be marked with PackedCountAttribute in order to be serialized.");
                }
                var aSize = info.GetFirstAttribute<PackedSizeAttribute>();
                if (aSize == null)
                {
                    throw new ArgumentException(info.ToString() +
                        " must be marked with PackedSizeAttribute in order to be serialized.");
                }
                Array arr = Array.CreateInstance(t.GetElementType(), aCount.Value);
                for (int i = 0; i < aCount.Value; i++)
                {
                    arr.SetValue(Deserialize(buffer, startIndex + i * aSize.Size,
                                 t.GetElementType(), info), 0);
                }
                return arr;
            }

            // Handle structures and classes.
            if (t.IsValueType || t.IsClass)
            {
                // Create an object of this type using the default constructor.
                // This works even with value types (such as struct) as it's
                // boxed in an object.
                object x = Activator.CreateInstance(t);

                // Set the fields and properties one by one.
                foreach (MemberInfo field in t.GetMembers(
                    BindingFlags.Instance | BindingFlags.Public))
                {
                    if (field.MemberType != MemberTypes.Field &&
                        field.MemberType != MemberTypes.Property)
                        continue;

                    // Skip the field if it's marked with PackedIgnoreAttribute.
                    if (field.GetFirstAttribute<PackedIgnoreAttribute>() != null)
                        continue;

                    // Skip the property if it's read-only or indexed.
                    if (field.MemberType == MemberTypes.Property &&
                        (!(field as PropertyInfo).CanWrite))
                        continue;

                    // Now the field must have the PackedOffsetAttribute.
                    var aOffset = field.GetFirstAttribute<PackedOffsetAttribute>();
                    if (aOffset == null)
                    {
                        throw new ArgumentException("Field " + t.Name + "." + field.Name +
                            " must be marked with PackedOffsetAttribute in order to be serialized.");
                    }
                    int offset = aOffset.Offset;

                    // Check if this field is marked with a PodSize attribute.
                    // The PodSize attribute is useful for arrays and strings.
                    var aSize = field.GetFirstAttribute<PackedSizeAttribute>();
                    int size = (aSize == null) ? -1 : aSize.Size;

                    // Deserialize this field based on its type.
                    object y = Deserialize(buffer, startIndex + offset,
                        field.GetFieldOrPropertyType(), field);
                    field.SetFieldOrPropertyValue(x, y);
                }
                return x;
            }

            // Now we're left with weird types such as ref and ptr.
            throw new NotSupportedException(
                "Type " + t.ToString() + " is not supported for deserialization.");
        }

        public static T Deserialize<T>(Stream stream)
        {
            Type type = typeof(T);

            // The type must have the PodSize attribute to state its size.
            PackedSizeAttribute attrObjSize = type.GetFirstAttribute<PackedSizeAttribute>();
            if (attrObjSize == null)
            {
                throw new ArgumentException("Type " + type.ToString() +
                    " must be marked with PodSizeAttribute in order to be deserialized.");
            }

            // Read that many bytes from the underlying stream.
            byte[] buffer = new byte[attrObjSize.Size];
            if (!stream.TryReadFull(buffer, 0, buffer.Length))
            {
                throw new InvalidDataException(
                    "The stream does not contain enough bytes to be deserialized.");
            }

            // Deserialize this object.
            return (T)Deserialize(buffer, 0, type, null);
        }
    }

    [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property,
                    AllowMultiple = false, Inherited = false)]
    public class PackedOffsetAttribute : Attribute
    {
        private int offset;

        public PackedOffsetAttribute(int offset)
        {
            if (offset < 0)
                throw new ArgumentException("offset must be greater than or equal to zero.");
            this.offset = offset;
        }

        public int Offset { get { return offset; } }
    }

    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct |
                    AttributeTargets.Field | AttributeTargets.Property,
                    AllowMultiple = false, Inherited = false)]
    public class PackedSizeAttribute : Attribute
    {
        private int size;

        public PackedSizeAttribute(int size)
        {
            if (size < 0)
                throw new ArgumentException("size must be greater than or equal to zero.");
            this.size = size;
        }

        public int Size { get { return size; } }
    }

    public class PackedCountAttribute : Attribute
    {
        private int value;
        public PackedCountAttribute(int value)
        {
            if (value <= 0)
                throw new ArgumentException("PackedCount must be greater than zero.");
            this.value = value;
        }
        public int Value { get { return value; } }
    }



    /// <summary>
    /// Indicates that a field or property should not be serialized.
    /// </summary>
    [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property,
                    AllowMultiple = false, Inherited = true)]
    public class PackedIgnoreAttribute : Attribute
    {
    }
}

#endif
